package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql" //	匿名导入包（在包路径前添加下画线“_”），主要用于引入一些驱动的init函数的初始化，只导入包但是不使用包内的类型和数据
)

/*
Go使用Mysql介绍
Golang提供了database/sql包，用于对SQL数据库的访问。它提供了一系列接口方法，用于访问关系数据库，但并不会提供数据库特有的方法，
那些特有的方法交给数据库驱动去实现。

	TIPS：
		1. 在安装MySQL模块之前需要先安装Git，如果没有安装Git，MySQL模块安装会失败；
		2. 导入包后就能调用该包中的数据和方法。但是对于数据库操作来说，开发者不应该直接使用导入的驱动包所提供的方法，
		   而应该使用 sql.DB对象所提供的统一的方法。因此在导入MySQL驱动时，使用了匿名导入包的方式；
		3. 在导入一个数据库驱动后，该驱动会自行初始化并注册到database/sql上下文中，就可以通过database/sql包所提供的方法来访问数据库
*/
func main() {

	/*
		一. 连接数据库之Open()函数
			1. driverName：使用的驱动名；
			2. dataSourceName：数据库连接信息。它包含了数据库的用户名、密码、数据库主机以及需要连接的数据库名等信息；
			3. sql.Open()返回的sql.DB对象是Goroutine并发安全的；
			4. sql.DB 帮助开发者管理数据库连接池。正在使用的连接被标记为繁忙，用完后回到连接池等待下次使用。如果开发者没有把连接释放回连接池，
			   会导致过多连接使系统资源耗尽；
			5. sql.DB的设计目标就是作为长连接（一次连接多次数据交互）使用，不宜频繁开关。比较好的做法是，为每个不同的datastore建一个DB对象，
			   保持这些对象打开。如果需要短连接（一次连接一次数据交互），就把DB作为参数传入function，而不要在function中开关
	*/
	var err error
	dbConn := DbConn{
		Dsn: "root:qinxh123456@tcp(122.51.88.186:3306)/golang_db?charset=utf8",
	}
	dbConn.Db, err = sql.Open("mysql", dbConn.Dsn)
	if err != nil {
		fmt.Println(err.Error())
		panic(any(err))
		// 此处跳出不需要 return 关键字，panic 会自动截断往下进行
	} else {
		fmt.Println("连接成功！")
	}
	// 延迟关闭db
	defer func(Db *sql.DB) {
		err := Db.Close()
		if err != nil {

		}
	}(dbConn.Db)

	/*
		二. 通过db.Exec() 实现新增、更新和删除操作
			TIPS：通过返回的err可知操作失败的原因，通过Result.RowsAffected()进一步查询本次插入数据影响的行数，
				  Result.LastInsertId()查询最后插入的ID（如果数据库支持查询最后插入ID）
	*/
	// 删除操作
	result, err := dbConn.Db.Exec("delete from user where name = ?", "steven")
	if err != nil {
		fmt.Println("error：", err.Error())
		return
	} else {
		if affected, err := result.RowsAffected(); err == nil {
			fmt.Println("影响的行数：", affected)
		}
	}
	// 新增操作
	exec, err := dbConn.Db.Exec("insert into user (name, password, phone) values (?, ?, ?)",
		"steven", "1234345", 13423456789)
	var thisId interface{}
	if err != nil {
		fmt.Println("error：", err.Error())
		return
	} else {
		if affected, err := exec.RowsAffected(); err == nil {
			fmt.Println("影响的行数：", affected)
		}

		if id, err := exec.LastInsertId(); err == nil {
			fmt.Println("最后插入的ID：", id)
			thisId = id
		}
	}
	// 更新操作
	r, err := dbConn.Db.Exec("update user set password = ? where id = ?", "更新后密码", thisId)
	if err != nil {
		fmt.Println("error：", err.Error())
		return
	} else {
		if affected, err := r.RowsAffected(); err == nil {
			fmt.Println("影响的行数：", affected)
		}
	}

	/*
		三. db.Query()方法实现查询数据。
			TIPS:
				1. 返回一个Rows作为查询结果；
				2. 将rows.Next()方法的返回值作为for循环的条件，迭代查询数据；
				3. 在循环中，通过 rows.Scan()方法读取每一行数据。
				   TIPS：rows.Scan()方法的参数顺序很重要，必须和查询结果的column相对应（数量和顺序都需要一致），不然会造成数据读取错误
	*/
	rows, err := dbConn.Db.Query("select name, password, phone, is_lock as isLock from user")
	if err != nil {
		fmt.Println("error：", err.Error())
		return
	} else {
		u := new(User)
		// 每次db.Query()操作后，都建议调用rows.Close()。因为db.Query()会从数据库连接池中获取一个连接，这个底层连接在结果集（rows）
		// 未关闭前会被标记为处于繁忙状态。当遍历读到最后一条记录时，会发生一个内部EOF错误，自动调用rows.Close()。
		// 但如果出现异常，提前退出循环，rows不会关闭，连接不会回到连接池中，连接也不会关闭，则此连接会一直被占用。
		// rows.Close()操作是幂等操作，所以即便对已关闭的rows再执行close()也没关系
		defer func(rows *sql.Rows) {
			err := rows.Close()
			if err != nil {

			}
		}(rows)
		for rows.Next() {
			if err := rows.Scan(&u.name, &u.password, &u.phone, &u.isLock); err != nil {
				panic(any(err))
			}
			fmt.Println(*u)
		}
	}

	/*
		四. db.QueryRow()方法查询单条数据
	*/
	u := new(User)
	// TIPS：这里rows.Scan()参数顺序和查询结果故意错位，可以看到数据读取错误
	err = dbConn.Db.QueryRow("select  password, name, phone, is_lock as isLock from user where id = ?",
		thisId).Scan(&u.name, &u.password, &u.phone, &u.isLock)
	if err != nil {
		fmt.Println("error：", err.Error())
		return
	} else {
		fmt.Println(*u)
	}

}

/*
user表的映射对象
*/
type User struct {
	name     string
	password string
	phone    int
	isLock   int
}

/*
定义数据库连接信息
*/
type DbConn struct {
	Dsn string // 数据库驱动连接字符串
	Db  *sql.DB
}
